﻿using System;
using System.Data.Entity;
using System.Threading;
using System.Web;

namespace Rail.Net.Data
{
    /// <summary>
    ///   This class provides several static methods for loading DataContext objects 
    ///   in a variety of ways. You can load the data context as normal one new instance
    ///   at a time, or you can choose to use one of the scoped factory methods that
    ///   can scope the DataContext to a WebRequest or a Thread context (in a WinForm app
    ///   for example).
    /// 
    ///   Using scoped variants can be more efficient in some scenarios and allows passing
    ///   a DataContext across multiple otherwise unrelated components so that the change
    ///   context can be shared.
    /// </summary>
    public class DbContextFactory
    {
        private static readonly object ObjSync = new object();

        /// <summary>
        ///   Creates a new Data Context for a specific DataContext type
        /// 
        ///   Provided merely for compleness sake here - same as new YourDataContext()
        /// </summary>
        /// <typeparam name = "TDataContext"></typeparam>
        /// <returns></returns>
        public static TDataContext GetDataContext<TDataContext>(Func<TDataContext> create)
            where TDataContext : DbContext, new()
        {
            return create();
        }

        /// <summary>
        ///   Creates a ASP.NET Context scoped instance of a DataContext. This static
        ///   method creates a single instance and reuses it whenever this method is
        ///   called.
        /// 
        ///   This version creates an internal request specific key shared key that is
        ///   shared by each caller of this method from the current Web request.
        /// </summary>
        public static TDataContext GetWebRequestScopedDataContext<TDataContext>(Func<TDataContext> create)
            where TDataContext : DbContext, new()
        {
            // *** Create a request specific unique key 
            return (TDataContext)GetWebRequestScopedDataContextInternal(null, create);
        }

        /// <summary>
        ///   Internal method that handles creating a context that is scoped to the HttpContext Items collection
        ///   by creating and holding the DataContext there.
        /// </summary>
        /// <param name = "key"></param>
        /// <param name="create"></param>
        /// <returns></returns>
        private static object GetWebRequestScopedDataContextInternal<TDataContext>(string key, Func<TDataContext> create)
            where TDataContext : DbContext
        {
            object context;

            if (HttpContext.Current == null)
            {
                context = create();
                return context;
            }

            // *** Create a unique Key for the Web Request/Context 
            if (key == null)
                key = "__WRSCDC_" + HttpContext.Current.GetHashCode().ToString("x") +
                      Thread.CurrentContext.ContextID;

            context = HttpContext.Current.Items[key];
            if (context == null)
            {
                context = create();
                HttpContext.Current.Items[key] = context;
            }
            return context;
        }

        /// <summary>
        ///   Creates a Thread Scoped DataContext object that can be reused.
        ///   The DataContext is stored in Thread local storage.
        /// </summary>
        /// <typeparam name = "TDataContext"></typeparam>
        /// <returns></returns>
        public static TDataContext GetThreadScopedDataContext<TDataContext>(Func<TDataContext> create)
            where TDataContext : DbContext, new()
        {
            return (TDataContext)GetThreadScopedDataContextInternal(null, create);
        }

        /// <summary>
        ///   Creates a Thread Scoped DataContext object that can be reused.
        ///   The DataContext is stored in Thread local storage.
        /// </summary>
        /// <param name = "key"></param>
        /// <param name="create"></param>
        /// <returns></returns>
        private static object GetThreadScopedDataContextInternal<TDataContext>(string key, Func<TDataContext> create)
            where TDataContext : DbContext
        {
            lock (ObjSync)
            {
                if (key == null)
                    key = "__WRSCDC_" + Thread.CurrentContext.ContextID;

                var threadData = Thread.GetNamedDataSlot(key);
                var context = Thread.GetData(threadData);
                if (context == null)
                {
                    context = create();
                    if (context != null)
                    {
                        Thread.SetData(threadData, context);
                    }
                }

                return context;
            }
        }

        /// <summary>
        ///   Returns either Web Request scoped DataContext or a Thread scoped
        ///   request object if not running a Web request (ie. HttpContext.Current)
        ///   is not available.
        /// </summary>
        /// <typeparam name = "TDataContext"></typeparam>
        /// <param name = "key"></param>
        /// <param name="create"></param>
        /// <returns></returns>
        public static TDataContext GetScopedDataContext<TDataContext>(string key, Func<TDataContext> create)
            where TDataContext : DbContext
        {
            if (HttpContext.Current != null)
                return (TDataContext)GetWebRequestScopedDataContextInternal(key, create);

            return (TDataContext)GetThreadScopedDataContextInternal(key, create);
        }


        /// <summary>
        ///   Returns either Web Request scoped DataContext or a Thread scoped
        ///   request object if not running a Web request (ie. HttpContext.Current)
        ///   is not available.
        /// </summary>
        /// <typeparam name = "TDataContext"></typeparam>
        /// <returns></returns>
        public static TDataContext GetScopedDataContext<TDataContext>(Func<TDataContext> create)
            where TDataContext : DbContext
        {
            if (HttpContext.Current != null)
                return (TDataContext)GetWebRequestScopedDataContextInternal(null, create);

            return (TDataContext)GetThreadScopedDataContextInternal(null, create);
        }
    }
}
